package de.uni_bremen.exceptions.oops;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import de.uni_bremen.exceptions.oops.ClassDeclaration.AccessModificator;

/**
 * Die Klasse realisiert die syntaktische Analyse für die folgende Grammatik. 
 * Terminale stehen dabei in Hochkommata oder sind groß geschrieben:
 * <pre>
 * program      ::= { classdecl }
 *
 * classdecl    ::= CLASS identifier [ EXTENDS identifier ] IS
 *                  { memberdecl } 
 *                  END CLASS
 *
 * memberdecl   ::= [PRIVATE | PROTECTED | PUBLIC]
 * 					( vardecl ';'
 *                | METHOD identifier [ '(' vardecl { ';' vardecl } ')' ]
 *                	[ ':' identifier ] IS methodbody )
 * 
 * vardecl      ::= identifier { ',' identifier } ':' identifier
 * 
 * methodbody   ::= { vardecl ';' }
 *                  BEGIN statements
 *                  END METHOD
 * 
 * statements   ::= { statement }
 * 
 * statement    ::= READ memberaccess ';'
 *                | WRITE oexpression ';'
 *                | IF oexpression 
 *                  THEN statements 
 *                  {ELSEIF oexpression THEN statements}
 *                  [ELSE statements]
 *                  END IF
 *                | WHILE oexpression 
 *                  DO statements 
 *                  END WHILE
 *                | memberaccess [ ':=' oexpression ] ';'
 *                | RETURN [ oexpression ] ';'
 * 
 * oexpression  ::= aexpression { 'OR' aexpression | 'OR ELSE' aexpression } 
 * 
 * aexpression  ::= relation { 'AND' relation | 'AND THEN' relation }
 * 
 * relation     ::= expression [ ( '=' | '#' | '<' | '>' | '<=' | '>=' ) expression ]
 * 
 * expression   ::= term { ( '+' | '-' ) term }
 * 
 * term         ::= factor { ( '*' | '/' | MOD ) factor }
 * 
 * factor       ::= ( '-' | 'NOT' ) factor
 *                | memberaccess
 * 
 * memberaccess ::= literal { '.' varorcall }
 * 
 * literal      ::= number
 *                | NULL
 *                | SELF
 *                | NEW identifier
 *                | '(' oexpression ')'
 *                | varorcall
 *                | TRUE
 *                | FALSE
 * 
 * varorcall    ::= identifier [ '(' oexpression { ',' oexpression } ')' ] 
 * </pre>
 * Daraus wird der Syntaxbaum aufgebaut, dessen Wurzel die Klasse
 * {@link Program Program} ist.
 */
class SyntaxAnalysis extends LexicalAnalysis
{
	/**
	 * Die Methode erzeugt einen "Unerwartetes Symbol"-Fehler.
	 * 
	 * @throws CompileException
	 *             Die entsprechende Fehlermeldung.
	 */
	private void unexpectedSymbol() throws CompileException
	{
		throw new CompileException("Unerwartetes Symbol " + symbol.id.toString(), symbol);
	}
	
	/**
	 * Die Methode überprüft, ob das aktuelle Symbol das erwartete ist. Ist dem so, wird das nächste Symbol gelesen,
	 * ansonsten wird eine Fehlermeldung erzeugt.
	 * 
	 * @param id
	 *            Das erwartete Symbol.
	 * @throws CompileException
	 *             Ein unerwartetes Symbol wurde gelesen.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private void expectSymbol(Symbol.Id id) throws CompileException, IOException
	{
		if (id != symbol.id)
		{
			unexpectedSymbol();
		}
		nextSymbol();
	}
	
	/**
	 * Die Methode überprüft, ob das aktuelle Symbol ein Bezeichner ist. Ist dem so, wird er zurückgeliefert, ansonsten
	 * wird eine Fehlermeldung erzeugt.
	 * 
	 * @throws CompileException
	 *             Ein unerwartetes Symbol wurde gelesen.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Identifier expectIdent() throws CompileException, IOException
	{
		if (symbol.id != Symbol.Id.IDENT)
		{
			unexpectedSymbol();
		}
		Identifier i = new Identifier(symbol.ident, new Position(symbol.line, symbol.column));
		nextSymbol();
		return i;
	}
	
	/**
	 * Die Methode überprüft, ob das aktuelle Symbol ein Bezeichner ist. Ist dem so, wird er in Form eines Bezeichners
	 * mit noch aufzulösender Vereinbarung zurückgeliefert, ansonsten wird eine Fehlermeldung erzeugt.
	 * 
	 * @throws CompileException
	 *             Ein unerwartetes Symbol wurde gelesen.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private ResolvableIdentifier expectResolvableIdent() throws CompileException, IOException
	{
		if (symbol.id != Symbol.Id.IDENT)
		{
			unexpectedSymbol();
		}
		ResolvableIdentifier r = new ResolvableIdentifier(symbol.ident, new Position(symbol.line, symbol.column));
		nextSymbol();
		return r;
	}
	
	/**
	 * Die Methode parsiert eine Klassendeklaration entsprechend der oben angegebenen Syntax und liefert diese zurück.
	 * 
	 * @return Die Klassendeklaration.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private ClassDeclaration classdecl() throws CompileException, IOException
	{
		expectSymbol(Symbol.Id.CLASS);
		ClassDeclaration c = new ClassDeclaration(expectIdent());
		if (symbol.id == Symbol.Id.EXTENDS)
		{
			nextSymbol();
			c.baseIdent = expectResolvableIdent();
		}
		expectSymbol(Symbol.Id.IS);
		while (symbol.id != Symbol.Id.END)
		{
			memberdecl(c.attributes, c.methods);
		}
		nextSymbol();
		expectSymbol(Symbol.Id.CLASS);
		return c;
	}
	
	/**
	 * Die Methode parsiert die Deklaration eines Attributs bzw. einer Methode entsprechend der oben angegebenen Syntax
	 * und hängt sie an eine von zwei Listen an.
	 * 
	 * @param attributes
	 *            Die Liste der Attributdeklarationen der aktuellen Klasse.
	 * @param methods
	 *            Die Liste der Methodendeklarationen der aktuellen Klasse.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private void memberdecl(HashMap<VarDeclaration, AccessModificator> attributes,
			HashMap<MethodDeclaration, AccessModificator> methods) throws CompileException, IOException
	{
		AccessModificator mod = AccessModificator.PUBLIC;
		switch (symbol.id)
		{
			case PRIVATE:
				nextSymbol();
				mod = AccessModificator.PRIVATE;
				break;
			case PROTECTED:
				nextSymbol();
				mod = AccessModificator.PROTECTED;
				break;
			case PUBLIC:
				nextSymbol();
				mod = AccessModificator.PUBLIC;
				break;
		}
		if (symbol.id == Symbol.Id.METHOD)
		{
			nextSymbol();
			MethodDeclaration m = new MethodDeclaration(expectIdent());
			if (symbol.id == Symbol.Id.LPAREN)
			{
				nextSymbol();
				vardecl(m.params, false, true);
				while (symbol.id == Symbol.Id.SEMICOLON)
				{
					nextSymbol();
					vardecl(m.params, false, true);
				}
				expectSymbol(Symbol.Id.RPAREN);
			}
			if (symbol.id == Symbol.Id.COLON)
			{
				nextSymbol();
				m.type = expectResolvableIdent();
			}
			expectSymbol(Symbol.Id.IS);
			methodbody(m.vars, m.statements);
			methods.put(m, mod);
		}
		else
		{
			vardecl(attributes, mod);
			expectSymbol(Symbol.Id.SEMICOLON);
		}
	}
	
	/**
	 * Die Methode parsiert die Deklaration eines Attributs bzw. einer Variablen entsprechend der oben angegebenen
	 * Syntax und hängt sie an eine Liste an.
	 * 
	 * @param vars
	 *            Die Liste der Attributdeklarationen der aktuellen Klasse oder der Variablen der aktuellen Methode.
	 * @param isAttribute
	 *            Ist die Variable ein Attribut?
	 * @param isParameter
	 *            Ist die Variable ein Parameter?
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private void vardecl(LinkedList<VarDeclaration> vars, boolean isAttribute, boolean isParameter)
			throws CompileException, IOException
	{
		LinkedList<VarDeclaration> temp = new LinkedList<VarDeclaration>();
		temp.add(new VarDeclaration(expectIdent(), isAttribute, isParameter));
		while (symbol.id == Symbol.Id.COMMA)
		{
			nextSymbol();
			temp.add(new VarDeclaration(expectIdent(), isAttribute, isParameter));
		}
		expectSymbol(Symbol.Id.COLON);
		ResolvableIdentifier ident = expectResolvableIdent();
		for (VarDeclaration v : temp)
		{
			v.type = ident;
			vars.add(v);
		}
	}
	
	/**
	 * Die Methode parsiert die Deklaration eines Attributs bzw. einer Variablen entsprechend der oben angegebenen
	 * Syntax und hängt sie an eine Liste an.
	 * 
	 * @param vars
	 *            Die Liste der Attributdeklarationen der aktuellen Klasse oder der Variablen der aktuellen Methode.
	 * @param mod
	 *            Gibt an, ob die Variable PUBLIC, PROTECTED oder PRIVATE ist.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private void vardecl(HashMap<VarDeclaration, AccessModificator> vars, AccessModificator mod)
			throws CompileException, IOException
	{
		LinkedList<VarDeclaration> temp = new LinkedList<VarDeclaration>();
		temp.add(new VarDeclaration(expectIdent(), true, false));
		while (symbol.id == Symbol.Id.COMMA)
		{
			nextSymbol();
			temp.add(new VarDeclaration(expectIdent(), true, false));
		}
		expectSymbol(Symbol.Id.COLON);
		ResolvableIdentifier ident = expectResolvableIdent();
		for (VarDeclaration v : temp)
		{
			v.type = ident;
			vars.put(v, mod);
		}
	}
	
	/**
	 * Die Methode parsiert die Deklaration eines Methodenrumpfes entsprechend der oben angegebenen Syntax. Lokale
	 * Variablendeklarationen und Anweisungen werden an die entsprechenden Listen angehängt.
	 * 
	 * @param vars
	 *            Die Liste der lokalen Variablendeklarationen der aktuellen Methode.
	 * @param statements
	 *            Die Liste der Anweisungen der aktuellen Methode.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private void methodbody(LinkedList<VarDeclaration> vars, LinkedList<Statement> statements) throws CompileException,
			IOException
	{
		while (symbol.id != Symbol.Id.BEGIN)
		{
			vardecl(vars, false, false);
			expectSymbol(Symbol.Id.SEMICOLON);
		}
		nextSymbol();
		statements(statements);
		expectSymbol(Symbol.Id.END);
		expectSymbol(Symbol.Id.METHOD);
	}
	
	/**
	 * Die Methode parsiert eine Folge von Anweisungen entsprechend der oben angegebenen Syntax und hängt sie an eine
	 * Liste an.
	 * 
	 * @param statements
	 *            Die Liste der Anweisungen.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private void statements(LinkedList<Statement> statements) throws CompileException, IOException
	{
		while (symbol.id != Symbol.Id.END)
		{
			statement(statements);
		}
	}
	
	/**
	 * Die Methode parsiert eine Anweisung entsprechend der oben angegebenen Syntax und hängt sie an eine Liste an.
	 * 
	 * @param statements
	 *            Die Liste der Anweisungen.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private void statement(LinkedList<Statement> statements) throws CompileException, IOException
	{
		switch (symbol.id)
		{
			case READ:
				nextSymbol();
				statements.add(new ReadStatement(memberAccess()));
				expectSymbol(Symbol.Id.SEMICOLON);
				break;
			case WRITE:
				nextSymbol();
				statements.add(new WriteStatement(oexpression()));
				expectSymbol(Symbol.Id.SEMICOLON);
				break;
			case IF:
				nextSymbol();
				IfStatement s = new IfStatement(oexpression());
				statements.add(s);
				expectSymbol(Symbol.Id.THEN);
				while (symbol.id != Symbol.Id.END && symbol.id != Symbol.Id.ELSEIF && symbol.id != Symbol.Id.ELSE)
					statement(s.thenStatements);
				while (symbol.id == Symbol.Id.ELSEIF)
				{
					nextSymbol();
					ElseIfStatement se = new ElseIfStatement(oexpression());
					s.elseIfStatement = se;
					s = se;
					expectSymbol(Symbol.Id.THEN);
					if (symbol.id != Symbol.Id.ELSE && symbol.id != Symbol.Id.ELSEIF)
					{
						while (symbol.id != Symbol.Id.END && symbol.id != Symbol.Id.ELSEIF
								&& symbol.id != Symbol.Id.ELSE)
							statement(s.thenStatements);
					}
				}
				if (symbol.id == Symbol.Id.ELSE)
				{
					nextSymbol();
					statements(s.elseStatements);
				}
				expectSymbol(Symbol.Id.END);
				expectSymbol(Symbol.Id.IF);
				break;
			case WHILE:
				nextSymbol();
				WhileStatement w = new WhileStatement(oexpression());
				statements.add(w);
				expectSymbol(Symbol.Id.DO);
				statements(w.statements);
				expectSymbol(Symbol.Id.END);
				expectSymbol(Symbol.Id.WHILE);
				break;
			case RETURN:
				nextSymbol();
				ReturnStatement r = new ReturnStatement();
				statements.add(r);
				if (symbol.id == Symbol.Id.SEMICOLON)
					nextSymbol();
				else
				{
					r.expression = oexpression();
					expectSymbol(Symbol.Id.SEMICOLON);
				}
				break;
			default:
				Expression e = memberAccess();
				if (symbol.id == Symbol.Id.BECOMES)
				{
					nextSymbol();
					statements.add(new Assignment(e, oexpression()));
				}
				else
				{
					statements.add(new CallStatement(e));
				}
				expectSymbol(Symbol.Id.SEMICOLON);
		}
	}
	
	/**
	 * Die Methode parsiert eine Disjunktion entsprechend der oben angegebenen Syntax und liefert ihn zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression oexpression() throws CompileException, IOException
	{
		Expression e = aexpression();
		while (symbol.id == Symbol.Id.OR)
		{
			Symbol.Id operator = symbol.id;
			nextSymbol();
			if (symbol.id == Symbol.Id.ELSE)
			{
				operator = Symbol.Id.ORELSE;
				nextSymbol();
			}
			e = new BinaryExpression(e, operator, aexpression());
		}
		return e;
	}
	
	/**
	 * Die Methode parsiert eine Konjunktion entsprechend der oben angegebenen Syntax und liefert ihn zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression aexpression() throws CompileException, IOException
	{
		Expression e = relation();
		while (symbol.id == Symbol.Id.AND)
		{
			Symbol.Id operator = symbol.id;
			nextSymbol();
			if (symbol.id == Symbol.Id.THEN)
			{
				operator = Symbol.Id.ANDTHEN;
				nextSymbol();
			}
			e = new BinaryExpression(e, operator, relation());
		}
		return e;
	}
	
	/**
	 * Die Methode parsiert eine Relation entsprechend der oben angegebenen Syntax und liefert den Ausdruck zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression relation() throws CompileException, IOException
	{
		Expression e = expression();
		switch (symbol.id)
		{
			case EQ:
			case NEQ:
			case GT:
			case GTEQ:
			case LT:
			case LTEQ:
				Symbol.Id operator = symbol.id;
				nextSymbol();
				e = new BinaryExpression(e, operator, expression());
		}
		return e;
	}
	
	/**
	 * Die Methode parsiert einen Ausdruck entsprechend der oben angegebenen Syntax und liefert ihn zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression expression() throws CompileException, IOException
	{
		Expression e = term();
		while (symbol.id == Symbol.Id.PLUS || symbol.id == Symbol.Id.MINUS)
		{
			Symbol.Id operator = symbol.id;
			nextSymbol();
			e = new BinaryExpression(e, operator, term());
		}
		return e;
	}
	
	/**
	 * Die Methode parsiert einen Term entsprechend der oben angegebenen Syntax und liefert den Ausdruck zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression term() throws CompileException, IOException
	{
		Expression e = factor();
		while (symbol.id == Symbol.Id.TIMES || symbol.id == Symbol.Id.DIV || symbol.id == Symbol.Id.MOD)
		{
			Symbol.Id operator = symbol.id;
			nextSymbol();
			e = new BinaryExpression(e, operator, factor());
		}
		return e;
	}
	
	/**
	 * Die Methode parsiert einen Faktor entsprechend der oben angegebenen Syntax und liefert den Ausdruck zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression factor() throws CompileException, IOException
	{
		switch (symbol.id)
		{
			case MINUS:
			case NOT:
				Symbol.Id operator = symbol.id;
				Position position = new Position(symbol.line, symbol.column);
				nextSymbol();
				return new UnaryExpression(operator, factor(), position);
			default:
				return memberAccess();
		}
	}
	
	/**
	 * Die Methode parsiert den Zugriff auf ein Objektattribut bzw. eine Objektmethode entsprechend der oben angegebenen
	 * Syntax und liefert den Ausdruck zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression memberAccess() throws CompileException, IOException
	{
		Expression e = literal();
		while (symbol.id == Symbol.Id.PERIOD)
		{
			nextSymbol();
			e = new AccessExpression(e, varorcall());
		}
		return e;
	}
	
	/**
	 * Die Methode parsiert den Aufruf einer Methode oder den Zugriff auf eine Variable.
	 * 
	 * @return Der Ausdruck
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private VarOrCall varorcall() throws CompileException, IOException
	{
		VarOrCall e = new VarOrCall(expectResolvableIdent());
		if (symbol.id == Symbol.Id.LPAREN)
		{
			nextSymbol();
			e.params.add(oexpression());
			while (symbol.id == Symbol.Id.COMMA)
			{
				nextSymbol();
				e.params.add(oexpression());
			}
			expectSymbol(Symbol.Id.RPAREN);
		}
		return e;
	}
	
	/**
	 * Die Methode parsiert ein Literal, die Erzeugung eines Objekts, einen geklammerten Ausdruck oder einen einzelnen
	 * Zugriff auf eine Variable, ein Attribut oder eine Methode entsprechend der oben angegebenen Syntax und liefert
	 * den Ausdruck zurück.
	 * 
	 * @return Der Ausdruck.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	private Expression literal() throws CompileException, IOException
	{
		Expression e = null;
		switch (symbol.id)
		{
			case NUMBER:
				e = new LiteralExpression(symbol.number, ClassDeclaration.intType, new Position(symbol.line,
						symbol.column));
				nextSymbol();
				break;
			case NULL:
				e = new LiteralExpression(0, ClassDeclaration.nullType, new Position(symbol.line, symbol.column));
				nextSymbol();
				break;
			case SELF:
				e = new VarOrCall(new ResolvableIdentifier("_self", new Position(symbol.line, symbol.column)));
				nextSymbol();
				break;
			case NEW:
				Position position = new Position(symbol.line, symbol.column);
				nextSymbol();
				e = new NewExpression(expectResolvableIdent(), position);
				break;
			case LPAREN:
				nextSymbol();
				e = oexpression();
				expectSymbol(Symbol.Id.RPAREN);
				break;
			case IDENT:
				e = varorcall();
				break;
			case TRUE:
				e = new LiteralExpression(1, ClassDeclaration.boolType, new Position(symbol.line, symbol.column));
				nextSymbol();
				break;
			case FALSE:
				e = new LiteralExpression(0, ClassDeclaration.boolType, new Position(symbol.line, symbol.column));
				nextSymbol();
				break;
			case BASE:
				e = new VarOrCall(new ResolvableIdentifier("_base", new Position(symbol.line, symbol.column)));
				nextSymbol();
				break;
			default:
				unexpectedSymbol();
		}
		return e;
	}
	
	/**
	 * Konstruktor.
	 * 
	 * @param fileName
	 *            Der Name des Quelltexts.
	 * @param printSymbols
	 *            Die lexikalische Analyse gibt die erkannten Symbole auf der Konsole aus.
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws FileNotFoundException
	 *             Der Quelltext wurde nicht gefunden.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	SyntaxAnalysis(String fileName, boolean printSymbols) throws CompileException, FileNotFoundException, IOException
	{
		super(fileName, printSymbols);
		ResolvableIdentifier.init();
	}
	
	/**
	 * Die Methode parsiert den Quelltext und liefert die Wurzel des Syntaxbaums zurück.
	 * 
	 * @throws CompileException
	 *             Der Quelltext entspricht nicht der Syntax.
	 * @throws IOException
	 *             Ein Lesefehler ist aufgetreten.
	 */
	Program parse() throws CompileException, IOException
	{
		nextSymbol();
		List<ClassDeclaration> classdecls = new ArrayList<ClassDeclaration>();
		while (symbol.id == Symbol.Id.CLASS)
			classdecls.add(classdecl());
		Program p = new Program(classdecls);
		expectSymbol(Symbol.Id.EOF);
		return p;
	}
}
